package com.hup.utils.algorithm.image;

import lombok.extern.log4j.Log4j2;

import java.awt.*;
import java.awt.image.BufferedImage;

/**
 * 魔棒算法;
 * <pre>
 * 用途如:
 * .截屏时,找出鼠标所在的图像区域;
 * .也可以用作寻找指定seed的连通域
 * 改写自c++语言,目前没有找到此代码算法的java版
 * 核心算法是,RegionGrow区域成长法,和[连通域算法]的Seed-Filling一样?
 * 参考资料:https://blog.csdn.net/real_myth/article/details/52597832
 *  资料中,比较两点容差值的代码是'有误的'(可能是需求不一样)
 * </pre>
 *
 * @author hugan
 * @date 2020/5/6
 */
@Log4j2
public class MagicWand {

    /**
     * 默认的容差处理器
     */
    public static final ThresholdHandler DEF_THRESHOLD_HANDLER = new ThresholdHandler() {
    };

    /**
     * 原始魔棒算法:根据[种子坐标],获取到扩展的区域
     *
     * @param data data[] of image
     * @param sX seed.x
     * @param sY seed.y
     * @param isEight 匹配方式:true-8邻域;false-4邻域
     * <pre>
     *      区别:4邻域是[识别条件更严格的],[结果区域更小的];<br>
     *      如[右,下]不连通,但[右下]连通时,8邻域的结果会扩展到[右下],4邻域不会
     * </pre>
     * @param thresholdHandler 容差判断器
     * @return seed所在区域:boolean[]-true代表区域
     */
    public static boolean[] wand(int[] data, int width, int height, int sX, int sY, boolean isEight,
            ThresholdHandler thresholdHandler) {
        boolean[] res = new boolean[data.length];
        //初始化 seed的位置为1, 其他为0
        res[sY * width + sX] = true;

        int st = 0, ed = 1;
        Node queue = new Node();
        queue.x = sX;
        queue.y = sY;
        Node first = queue;
        Node last = queue;

        int seedValue = data[sY * width + sX];//种子点的色值
        while (ed > st) {
            int x = first.x;
            int y = first.y;
            //遍历8/4邻域的点
            for (int yy = -1; yy <= 1; yy++) {
                for (int xx = -1; xx <= 1; xx++) {
                    if (!isEight && yy * xx != 0) {
                        //4邻域,且是四个对角:则跳过识别
                        continue;
                    }

                    int cx = x + xx;
                    int cy = y + yy;
                    if (cx >= 0 && cx < width && cy >= 0 && cy < height) {
                        //邻域点在图像范围内
                        if (!res[cy * width + cx] && thresholdHandler.matchThreshold(data[cy * width + cx], seedValue)) {
                            //没被标记过,且符合容差条件
                            Node n = new Node();
                            n.x = cx;
                            n.y = cy;
                            ed++;
                            /*
                            这行不是多余的:
                            虽然后面直接重新赋值last,
                            但这里赋值last.next是更新原来的last的(如queue)
                             */
                            last.next = n;
                            last = n;

                            res[cy * width + cx] = true;
                            thresholdHandler.debugMarked(res, width, height, sX, sY);
                        }
                    }
                }
            }
            first = first.next;
            st++;
        }

        return res;
    }

    /**
     * 容差判断器
     */
    public interface ThresholdHandler {
        /**
         * 默认容差值:目前的容差算法,是没有比较通用的容差值的;
         * 但针对特定图像(如微信头像,在截图工具中)动态调节容差时,是可以找到可以选取[整图]的容差的
         */
        int DEFAULT_RGB_THRESHOLD = 8;

        /**
         * @return rgb色彩的容差, 范围[0, 255]
         */
        default int getRgbThreshold() {
            return DEFAULT_RGB_THRESHOLD;
        }

        /**
         * rgb数据的容差判断算法
         *
         * @param targetValue 当前被判断的值
         * @param seedValue 种子点的值
         */
        default boolean matchRgbThreshold(int targetValue, int seedValue) {
            //自推的算法,与PS的效果好像是一样的:rgb各自的差值与容差值做比较
            int r1 = Math.abs(((targetValue >> 16) & 0xff) - ((seedValue >> 16) & 0xff));
            int r2 = Math.abs(((targetValue >> 8) & 0xff) - ((seedValue >> 8) & 0xff));
            int r3 = Math.abs(((targetValue) & 0xff) - ((seedValue) & 0xff));
            int threshold = getRgbThreshold();
            return r1 <= threshold
                    && r2 <= threshold
                    && r3 <= threshold;
        }

        /**
         * 判断两个点是否符合容差条件(差异值小于容差)
         *
         * @param targetValue 当前被判断的值
         * @param seedValue 种子点的值
         * @return true-符合容差条件,是连通域
         */
        default boolean matchThreshold(int targetValue, int seedValue) {
            return matchRgbThreshold(targetValue, seedValue);
        }

        /**
         * 调试,找到新的领域点后触发
         *
         * @param res 当前所有领域点
         */
        default void debugMarked(boolean[] res, int width, int height, int sX, int sY) {
        }
    }

    private static class Node {
        private int x, y;
        private Node next;
    }

    /**
     * {@link MagicWand}使用帮助类
     */
    public static class Helper {

        public static Helper create() {
            return new Helper();
        }

        /**
         * 图像数据
         */
        private int width;
        private int height;
        private int[] data;
        private boolean isEight = true;
        private ThresholdHandler thresholdHandler = DEF_THRESHOLD_HANDLER;

        /**
         * @param bf 彩图图像数据,如果是同一张图频繁识别时,要使用 {@link #init(int[], int)}
         */
        public Helper init(BufferedImage bf) {
            width = bf.getWidth();
            height = bf.getHeight();
            data = bf.getRGB(0, 0, width, height, null, 0, width);
            return this;
        }

        /**
         * @param data rgb数据,如:screenShot.getRGB(0, 0, screenShot.getWidth(), screenShot.getHeight(), null, 0, screenShot.getWidth());
         */
        public Helper init(int[] data, int width) {
            this.data = data;
            this.width = width;
            this.height = data.length / width;
            if (width * height != data.length) {
                throw new IllegalArgumentException("入参数据错误?!");
            }
            return this;
        }

        /**
         * @param isEight 匹配方式:true-8邻域;false-4邻域
         * <pre>
         *      区别:4邻域是[识别条件更严格的],[结果区域更小的];<br>
         *      如[右,下]不连通,但[右下]连通时,8邻域的结果会扩展到[右下],4邻域不会
         * </pre>
         * @apiNote 简单测试时, 不同模式下耗时差距不大
         */
        public Helper isEight(boolean isEight) {
            this.isEight = isEight;
            return this;
        }

        public Helper thresholdHandler(ThresholdHandler thresholdHandler) {
            this.thresholdHandler = thresholdHandler;
            return this;
        }

        /**
         * 释放data内存
         */
        public void flush() {
            data = null;
        }

        /**
         * @return seed所在区域:boolean[]-true代表区域
         */
        public boolean[] getBoolean(int sX, int sY) {
            if (data == null) throw new IllegalArgumentException("未初始化数据");
            return wand(data, width, height, sX, sY, isEight, thresholdHandler);
        }

        /**
         * @return seed所在的边框(边框内不全是seed的邻域)
         * @apiNote 宽高计算时, 是要+1的!
         * java.awt.image.BufferedImage.getSubimage时是正确截取到图像的;
         * 但 java.awt.Graphics.drawRect方法是畸形的(bug??),传入的宽高为1时,画出来的矩形宽高是2
         */
        @SuppressWarnings("ConstantConditions")
        public Rectangle getRect(int sX, int sY) {
            if (data == null) throw new IllegalArgumentException("未初始化数据");
            boolean[] area = getBoolean(sX, sY);
            int x1, x2, y1, y2;//边界坐标
            x1 = x2 = sX;
            y1 = y2 = sY;
            for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                    if (area[y * width + x]) {
                        if (x < x1) x1 = x;
                        else if (x > x2) x2 = x;
                        if (y < y1) y1 = y;
                        else if (y > y2) y2 = y;
                    }
                }
            }
            /*

             */
            //log.info("x1={}, x2={}, y1={}, y2={}, sX={}, sY={}", x1, x2, y1, y2, sX, sY);
            Rectangle res = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
            log.debug("res={}", res);
            return res;
        }

    }

}

